package com.hup.utils.commons;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import sun.misc.BASE64Decoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.Security;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;

/**
 * @author hugan
 * @date 2023/8/8
 */
@Slf4j
public class EncryptUtil {

    /**
     * 生成RSA密钥对;
     *
     * @apiNote 生成的密钥对 的密钥格式:"?"
     */
    public static void rsaCreateKeys() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048); // 设置密钥长度
        KeyPair keyPair = keyPairGenerator.genKeyPair();

        // 获取公钥和私钥的字节数组
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();

        // 将字节数组用Base64编码成字符串
        String publicKey = Base64.getEncoder().encodeToString(publicKeyBytes);
        String privateKey = Base64.getEncoder().encodeToString(privateKeyBytes);
        log.info("publicKey=\n{}", publicKey);
        log.info("privateKey=\n{}", privateKey);
    }

    /**
     * AES 生成随机字符串
     */
    public static void aesCreateKeys() {
        int resLength = 32;//key
        //int resLength = 16;//iv
        StringBuilder sb = new StringBuilder(resLength);

        String seed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        ThreadLocalRandom random = ThreadLocalRandom.current();
        for (int i = 0; i < resLength; i++) {
            int index = random.nextInt(62);
            //log.info("index={}", index);
            sb.append(seed.charAt(index));
        }
        log.info("res={}", sb.toString());
    }

    public static PrivateKey rsaPrivateKey(String privateKeyStr) throws Exception {
        KeyFactory factory = KeyFactory.getInstance("RSA");
        byte[] bytes = new BASE64Decoder().decodeBuffer(privateKeyStr);
        return factory.generatePrivate(new PKCS8EncodedKeySpec(bytes));
    }

    public static PublicKey rsaPublicKey(String publicKey) throws Exception {
        KeyFactory factory = KeyFactory.getInstance("RSA");
        byte[] bytes = new BASE64Decoder().decodeBuffer(publicKey);
        return factory.generatePublic(new X509EncodedKeySpec(bytes));
    }

    /**
     * 公钥 加密数据
     *
     * @param data 支持中文
     * @throws Exception 处理异常,密钥对异常,数据异常
     */
    public static String rsaEncrypt(PublicKey publicKey, String data) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 私钥 解密数据
     *
     * @return 支持中文
     * @throws Exception 处理异常,密钥对异常,数据异常
     */
    public static String rsaDecrypt(PrivateKey privateKey, String data) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return new String(cipher.doFinal(Base64.getDecoder().decode(data.getBytes())));
    }

    /**
     * 加密 {@link AESParam}
     *
     * @throws Exception 处理异常,密钥对异常,数据异常
     */
    public static String aesEncrypt(AESParam aesParam) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(aesParam.key.getBytes(StandardCharsets.UTF_8), "AES");
        Cipher cipher = Cipher.getInstance(aesParam.algorithm);
        if (aesParam.iv == null) {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        } else {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(aesParam.iv.getBytes()));
        }
        byte[] encryptedBytes = cipher.doFinal(aesParam.data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    /**
     * 解密 {@link AESParam}
     *
     * @throws Exception 处理异常,密钥对异常,数据异常
     */
    public static String aesDecrypt(AESParam aesParam) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(aesParam.key.getBytes(StandardCharsets.UTF_8), "AES");
        Cipher cipher = Cipher.getInstance(aesParam.algorithm);
        if (aesParam.iv == null) {
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
        } else {
            cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(aesParam.iv.getBytes()));
        }
        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(aesParam.data));
        return new String(decryptedBytes, StandardCharsets.UTF_8);
    }

    /**
     * AES
     * https://blog.csdn.net/m0_69916115/article/details/126750612
     * https://blog.csdn.net/javasimawanyi/article/details/131749628
     * ===
     * 密钥字符串
     * 长度要求：128 比特（16 字节），192 比特（24 字节）或 256 比特（32 字节）
     * ===
     * MODE（模式）指定了在加密或解密过程中如何处理数据块的运算模式
     * ECB: 相同的明文将产生相同的密文, 一般不推荐
     * CBC: CBC 模式通常更安全, 需要传入[初始化向量,iv, Initialization Vector, 类似MD5中的盐值]
     * ===
     * PADDING（填充）用于确定加密数据块的长度，因为 AES 是对固定长度的数据块进行加密，如果数据块长度不足，就需要对数据进行填充
     * PKCS5Padding：使用 PKCS#5 标准的填充模式，适用于分组长度为 8 字节（64 位）的加密算法，如 AES。
     * PKCS7Padding：与 PKCS5Padding 类似，只是应用于分组长度不只为 8 字节的加密算法。
     * NoPadding：不进行填充，要求明文长度必须是分组长度的整数倍。如果不满足要求，需要在外部手动进行处理。
     */
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public static class AESParam {

        private static boolean initAlgorithmExtend = false;

        /**
         * 初始化JDK8没有的算法
         *
         * @apiNote 需要引入依赖!! 见方法体
         */
        private static void initAlgorithmExtend() {
            /*
            <dependency>
                <groupId>org.bouncycastle</groupId>
                <artifactId>bcprov-jdk15on</artifactId>
                <version>1.70</version>
            </dependency>
             */
            if (initAlgorithmExtend) return;
            try {
                /*
                反射内容 等同于 Security.addProvider(new BouncyCastleProvider());
                反射目的: 不使用PKCS7Padding时,可以不需要引入依赖,代码不会编译异常
                 */
                Class<?> clz = Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
                Constructor<?> constructor = clz.getConstructor();
                Object provider = constructor.newInstance();
                Security.addProvider((Provider) provider);
                initAlgorithmExtend = true;
            } catch (Exception e) {
                log.error("初始化AES模块错误, 需要引入依赖?", e);
            }
        }

        private String algorithm;
        private String key;
        private String iv;
        private String data;

        /**
         * @param key 密钥, 加解密时传入不同时,会报错
         * @param iv 初始化向量, CBC时使用, 长度必须是16; 加解密时传入不同时, 不会报错,只是解密结果会'局部不准确'
         * @param data 支持中文
         */
        public static AESParam CBC_PKCS7Padding(String key, String iv, String data) {
            initAlgorithmExtend();
            AESParam res = new AESParam();
            res.algorithm = "AES/CBC/PKCS7Padding";
            res.key = key;
            res.iv = iv;
            res.data = data;
            return res;
        }

        /**
         * @param key 密钥, 加解密时传入不同时,会报错
         * @param iv 初始化向量, CBC时使用, 长度必须是16; 加解密时传入不同时, 不会报错,只是解密结果会'局部不准确'
         * @param data 支持中文
         */
        public static AESParam CBC_PKCS5Padding(String key, String iv, String data) {
            AESParam res = new AESParam();
            res.algorithm = "AES/CBC/PKCS5Padding";
            res.key = key;
            res.iv = iv;
            res.data = data;
            return res;
        }

        /**
         * @param key 密钥, 加解密时传入不同时,会报错
         * @param data 支持中文
         */
        public static AESParam ECB_PKCS7Padding(String key, String data) {
            initAlgorithmExtend();
            AESParam res = new AESParam();
            res.algorithm = "AES/ECB/PKCS7Padding";
            res.key = key;
            res.data = data;
            return res;
        }

        /**
         * @param key 密钥, 加解密时传入不同时,会报错
         * @param data 支持中文
         */
        public static AESParam ECB_PKCS5Padding(String key, String data) {
            AESParam res = new AESParam();
            res.algorithm = "AES/ECB/PKCS5Padding";
            res.key = key;
            res.data = data;
            return res;
        }

    }

}
